// Copyright 2012 Jetro Lauha - http://iki.fi/jetro/

#ifdef _WIN32
#include <Windows.h>
#endif
#include "GL/GL.h"

#include <irrlicht.h>
#include <cstring>
#include <cassert>
#include <cstdio>
#include "demo.h"
#include "fx_logopts.h"


#define VTX_COLOR   0xff112211
#define CENTER_X    0
#define CENTER_Y    0
#define SCALE_X     0.003f
#define SCALE_Y     -0.003f


using namespace irr;


using namespace std;

#define WBS_FILE_VERSION    2


struct TracePoint
{
    float x, y;
};

struct Trace
{
    Trace() : npoints(0), points(0) {}
    ~Trace() { destroy(); }

    void destroy();

    int npoints;
    TracePoint *points;
};

struct Layer
{
    Layer() : ntraces(0), traces(0) {}
    ~Layer() { destroy(); }

    void destroy();

    int ntraces;
    Trace *traces;
};

struct Sketch
{
    Sketch() : nlayers(0), layers(0) {}
    ~Sketch() { destroy(); }

    float xmin, ymin, xmax, ymax;
    int nlayers;
    Layer *layers;

    void parse(const char *wbs, int length);
    void destroy();
};


void Trace::destroy()
{
    if (points)
    {
        delete[] points;
        npoints = 0;
        points = 0;
    }
}

void Layer::destroy()
{
    if (traces)
    {
        delete[] traces;
        ntraces = 0;
        traces = 0;
    }
}

void Sketch::destroy()
{
    if (layers)
    {
        delete[] layers;
        nlayers = 0;
        layers = 0;
    }
}

void Sketch::parse(const char *wbs, int length)
{
    destroy();

    assert(wbs);
    if (wbs == 0)
        return;

    static const char sWbsFileMagic[4] = { 'w', 'b', 's', 26 };
    assert(memcmp(sWbsFileMagic, wbs, 4) == 0);
    int memRead_offset = 4; // skip magic header
#define MEMREAD(dest, size) do { assert(memRead_offset <= length - (size)); memcpy(dest, wbs + memRead_offset, size); memRead_offset += size; } while (0)

    int fileVersion = 0;
    MEMREAD(&fileVersion, 4);
    assert(fileVersion == WBS_FILE_VERSION);

    unsigned int pointDataFlags = 0;
    MEMREAD(&pointDataFlags, 4);

    MEMREAD(&xmin, 4);
    MEMREAD(&ymin, 4);
    MEMREAD(&xmax, 4);
    MEMREAD(&ymax, 4);

    MEMREAD(&nlayers, 4);
    layers = new Layer[nlayers];
    assert(layers);
    for (int layerIdx = 0; layerIdx < nlayers; ++layerIdx)
    {
        Layer &layer = layers[layerIdx];
        unsigned short nameLength;
        MEMREAD(&nameLength, 2);
        memRead_offset += nameLength; // skip name
        MEMREAD(&layer.ntraces, 4);
        layer.traces = new Trace[layer.ntraces];
        assert(layer.traces);
        for (int traceIdx = 0; traceIdx < layer.ntraces; ++traceIdx)
        {
            Trace &trace = layer.traces[traceIdx];
            MEMREAD(&trace.npoints, 4);
            trace.points = new TracePoint[trace.npoints];
            assert(trace.points);
            for (int ptIdx = 0; ptIdx < trace.npoints; ++ptIdx)
            {
                TracePoint &tp = trace.points[ptIdx];
                MEMREAD(&tp.x, 4);
                MEMREAD(&tp.y, 4);
                // skip unused stuff if present:
                if (pointDataFlags & 0x2)
                    memRead_offset += 2;
                if (pointDataFlags & 0x4)
                    memRead_offset += 2;
            }
        }
    }

#undef MEMREAD
}


class LogoPtsShaderCallBack : public video::IShaderConstantSetCallBack
{
public:
	core::vector3df mLensPos;

    virtual void OnSetConstants(video::IMaterialRendererServices *services, s32 userData)
    {
        video::IVideoDriver *driver = services->getVideoDriver();

        core::matrix4 invWorld = driver->getTransform(video::ETS_WORLD);
        invWorld.makeInverse();

        services->setVertexShaderConstant("mInvWorld", invWorld.pointer(), 16);

        core::matrix4 worldViewProj;
        worldViewProj = driver->getTransform(video::ETS_PROJECTION);
        worldViewProj *= driver->getTransform(video::ETS_VIEW);
        worldViewProj *= driver->getTransform(video::ETS_WORLD);

        services->setVertexShaderConstant("mWorldViewProj", worldViewProj.pointer(), 16);

        core::matrix4 world = driver->getTransform(video::ETS_WORLD);
        world = world.getTransposed();

        services->setVertexShaderConstant("mTransWorld", world.pointer(), 16);

		services->setVertexShaderConstant("mLensPos", reinterpret_cast<f32*>(&mLensPos), 3);

		/*
        // set texture, for textures it's very important to pass an int instead of float for a setPixelShaderConstant method.
        s32 TextureLayerID = 0;
        if (UseHighLevelShaders)
            services->setPixelShaderConstant("myTexture", &TextureLayerID, 1);
		*/
    }
};


static Sketch sSketch;

static scene::ISceneManager *sSceneManager = 0;
static scene::IMeshSceneNode *sLogo = 0;

static LogoPtsShaderCallBack sLogoPtsShaderCallBack;


bool LogoPtsFx::init()
{
    const char *fn = "data/logopts.wbs";
    //const char *fn = "data/logopts_minitest.wbs";

    io::IFileSystem *fs = gIrrDevice->getFileSystem();
    io::IReadFile *f = fs->createAndOpenFile(io::path(fn));
    if (f == 0)
    {
        gErrorMessage("Error/logopts", "File not found: %s", fn);
        return false;
    }
    long size = f->getSize();
    char *data = new char[size];
    if (!data)
    {
        f->drop();
        gErrorMessage("Error/logopts", "Can't alloc %d bytes for loading file", size);
        return false;
    }
    s32 rdcount = f->read(data, (u32)size);
    assert(rdcount == (s32)size);
    f->drop();
    f = 0;

    sSketch.parse(data, size);
    delete[] data;


    scene::SMeshBuffer *mb = new scene::SMeshBuffer;
    video::S3DVertex vtx;
    vtx.Normal.set(0, 0, 0);
    vtx.TCoords.set(0, 0);
    vtx.Color.set(VTX_COLOR);

    Sketch &sketch = sSketch;
    float xc, yc;
    xc = (sketch.xmax + sketch.xmin) / 2;
    yc = (sketch.ymax + sketch.ymin) / 2;
    int index = 0;
    for (int li = 0; li < sketch.nlayers; ++li)
    {
        Layer &layer = sketch.layers[li];
        for (int ti = 0; ti < layer.ntraces; ++ti)
        {
            Trace &trace = layer.traces[ti];
            for (int pi = 0; pi < trace.npoints; ++pi)
            {
                if (index < 65536)
                {
                    TracePoint &tpt = trace.points[pi];
                    vtx.Pos.set((tpt.x - xc) * SCALE_X + CENTER_X, (tpt.y - yc) * SCALE_Y + CENTER_Y, 0);
                    mb->Vertices.push_back(vtx);
                    mb->Indices.push_back((u16)index);
                }
                ++index;
            }
        }
    }
    if (index >= 65536)
        gErrorMessage("Warning/logopts", "Too many points: %d", index);

    mb->getMaterial().setFlag(video::EMF_POINTCLOUD, true);
    mb->recalculateBoundingBox();

    mb->setHardwareMappingHint(scene::EHM_STATIC);
    scene::SMesh *mesh = new scene::SMesh;
    mesh->addMeshBuffer(mb);
    mesh->recalculateBoundingBox();
    mb->drop();

    core::vector3df cameraPosition(0, 0.0f, -5.0f);

    sSceneManager = gIrrDevice->getSceneManager()->createNewSceneManager();
    sSceneManager->addCameraSceneNode();
    sSceneManager->getActiveCamera()->setPosition(cameraPosition);

    //sLogo = sSceneManager->addCubeSceneNode(1);
    sLogo = sSceneManager->addMeshSceneNode(mesh);
    mesh->drop();

	s32 materialType;

    video::IGPUProgrammingServices *gpups = gIrrDevice->getVideoDriver()->getGPUProgrammingServices();
	materialType = gpups->addHighLevelShaderMaterialFromFiles("data/fx_logopts.vs", "data/fx_logopts.fs", &sLogoPtsShaderCallBack, video::EMT_TRANSPARENT_ADD_COLOR, 0);

    video::SMaterial &mat = sLogo->getMaterial(0);
    mat.setFlag(video::EMF_LIGHTING, false);
    mat.setFlag(video::EMF_POINTCLOUD, true);
    mat.setFlag(video::EMF_BACK_FACE_CULLING, false);
    mat.setFlag(video::EMF_ZWRITE_ENABLE, false);
    //mat.MaterialType = video::EMT_ONETEXTURE_BLEND;
    //mat.MaterialTypeParam = video::pack_textureBlendFunc(video::EBF_ONE, video::EBF_ONE, video::EMFN_MODULATE_1X, video::EAS_NONE);
    //mat.MaterialType = video::EMT_TRANSPARENT_ADD_COLOR;
	mat.MaterialType = (video::E_MATERIAL_TYPE)materialType;
    mat.Thickness = 8;

    mInitialized = true;
    return true;
}


void LogoPtsFx::render(double time, int timeMs)
{
    if (!sLogo || !sSceneManager)
        return;

    {
        // HACK: draw one zero-sized triangle to force some gfx driver state reset, so that the pts effect renders properly
        video::SMaterial mat;
        sSceneManager->getVideoDriver()->setMaterial(mat);
        video::S3DVertex v[3];
        v[0].Pos.set(0, 0, 0);
        v[1].Pos = v[2].Pos = v[0].Pos;
        const static u16 ind[3] = { 0, 1, 2 };
        sSceneManager->getVideoDriver()->drawVertexPrimitiveList(v, 3, ind, 1);
    }

	// hack
	#define GL_VERTEX_PROGRAM_POINT_SIZE      0x8642
	glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);

    core::vector3df eulerAngles(0, (float)((time - mFxStartTime) * 5.0 - 90), 0);
    sLogo->setRotation(eulerAngles);
    sSceneManager->drawAll();
	float nofxSecs = 2.0f;
	float amount = core::clamp<float>((float)(((time - mFxStartTime - nofxSecs) / 5.0)), 0, 1);
	sLogoPtsShaderCallBack.mLensPos.set(cosf((float)time * 2) * (sSketch.xmax - sSketch.xmin) * SCALE_X * 0.5f,
										sinf((float)time * 2) * (sSketch.ymax - sSketch.ymin) * SCALE_Y * 0.5f,
										amount);

    // hack
    glDisable(GL_VERTEX_PROGRAM_POINT_SIZE);
}


void LogoPtsFx::deinit()
{
    if (sSceneManager)
        sSceneManager->drop();
    sSceneManager = 0;
    sSketch.destroy();
    mInitialized = false;
}
